🎖️GitЯра🎖️
Node / meshtastic / Meshtastic-Android / files / feature / map / src / commonMain / kotlin / org / meshtastic / feature / map / kml / KmlGeometry.kt
Displaying Raw • Download
feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/kml/KmlGeometry.kt renovate/fastlane-2.x-lockfile (056eed64) Text, 6.57 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.map.kml
Tff7b72import T7ee787kotlin.math.abs
Tff7b72import T7ee787kotlin.math.sign
Tff7b72internal Tff7b72fun Td2a8ffpointGeometryTb4b4b4(Te6edf3positionTb4b4b4: Te6edf3GeoPositionTb4b4b4) Tff7b72= Te6edf3KmlGeometryTb4b4b4(Ta5d6ff"Ta5d6ffPointTa5d6ff"Tb4b4b4, Te6edf3positionTb4b4b4.Te6edf3jsonTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3isPolygonal Tff7b72= Tff7b72falseTb4b4b4)
T8b949e/**
* A line, split into a MultiLineString if it crosses the antimeridian.
*
* RFC 7946 §3.1.9 asks for the cut, and without it a track between, say, Fiji and Samoa draws as a line back
* across the entire world rather than the short hop it is.
*/
Tff7b72internal Tff7b72fun Td2a8fflineGeometryTb4b4b4(Te6edf3positionsTb4b4b4: Te6edf3ListTff7b72<Te6edf3GeoPositionTff7b72>Tb4b4b4)Tb4b4b4: Te6edf3KmlGeometry Tb4b4b4{
Tff7b72val Te6edf3segments Tff7b72= Te6edf3positionsTb4b4b4.Te6edf3splitAtAntimeridianTb4b4b4(Tb4b4b4)
Tff7b72return Tff7b72if Tb4b4b4(Te6edf3segmentsTb4b4b4.Te6edf3size Tff7b72=Tff7b72= T79c0ff1Tb4b4b4) Tb4b4b4{
Te6edf3KmlGeometryTb4b4b4(Ta5d6ff"Ta5d6ffLineStringTa5d6ff"Tb4b4b4, Te6edf3segmentsTb4b4b4.Te6edf3singleTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3jsonTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3isPolygonal Tff7b72= Tff7b72falseTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3KmlGeometryTb4b4b4(
Te6edf3type Tff7b72= Ta5d6ff"Ta5d6ffMultiLineStringTa5d6ff"Tb4b4b4,
Te6edf3coordinates Tff7b72= Te6edf3segmentsTb4b4b4.Te6edf3joinToStringTb4b4b4(Ta5d6ff"Ta5d6ff,Ta5d6ff"Tb4b4b4, Te6edf3prefix Tff7b72= Ta5d6ff"Ta5d6ff[Ta5d6ff"Tb4b4b4, Te6edf3postfix Tff7b72= Ta5d6ff"Ta5d6ff]Ta5d6ff"Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3jsonTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4,
Te6edf3isPolygonal Tff7b72= Tff7b72falseTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72internal Tff7b72fun Td2a8ffpolygonGeometryTb4b4b4(Te6edf3ringTb4b4b4: Te6edf3ListTff7b72<Te6edf3GeoPositionTff7b72>Tb4b4b4)Tb4b4b4: Te6edf3KmlGeometry Tb4b4b4{
T8b949e// RFC 7946 §3.1.6: an exterior ring winds counterclockwise. KML says nothing about winding and exporters go both
T8b949e// ways, so the ring is turned rather than trusted.
Tff7b72val Te6edf3wound Tff7b72= Tff7b72if Tb4b4b4(Te6edf3ringTb4b4b4.Te6edf3isCounterClockwiseTb4b4b4(Tb4b4b4)Tb4b4b4) Te6edf3ring Tff7b72else Te6edf3ringTb4b4b4.Te6edf3reversedTb4b4b4(Tb4b4b4)
T8b949e// GeoJSON requires a closed ring; KML usually closes its own, but not always.
Tff7b72val Te6edf3closed Tff7b72= Tff7b72if Tb4b4b4(Te6edf3woundTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4) Tff7b72=Tff7b72= Te6edf3woundTb4b4b4.Te6edf3lastTb4b4b4(Tb4b4b4)Tb4b4b4) Te6edf3wound Tff7b72else Te6edf3wound Tff7b72+ Te6edf3woundTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3KmlGeometryTb4b4b4(Te6edf3type Tff7b72= Ta5d6ff"Ta5d6ffPolygonTa5d6ff"Tb4b4b4, Te6edf3coordinates Tff7b72= Ta5d6ff"Ta5d6ff[Tffd700${Te6edf3closedTb4b4b4.Te6edf3jsonTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff]Ta5d6ff"Tb4b4b4, Te6edf3isPolygonal Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72fun Te6edf3GeoPositionTb4b4b4.Td2a8ffjsonTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657String Tff7b72= Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3longitudeTa5d6ff,Tffd700$Te6edf3latitudeTa5d6ff]Ta5d6ff"
Tff7b72private Tff7b72fun Td2a8ffListTff7b72<Te6edf3GeoPositionTff7b72>Tb4b4b4.Te6edf3jsonTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657String Tff7b72= Te6edf3joinToStringTb4b4b4(Ta5d6ff"Ta5d6ff,Ta5d6ff"Tb4b4b4, Te6edf3prefix Tff7b72= Ta5d6ff"Ta5d6ff[Ta5d6ff"Tb4b4b4, Te6edf3postfix Tff7b72= Ta5d6ff"Ta5d6ff]Ta5d6ff"Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3jsonTb4b4b4(Tb4b4b4) Tb4b4b4}
T8b949e/**
* The ring's winding, by the shoelace sum.
*
* A positive sum is a clockwise ring in a longitude/latitude frame, so counterclockwise — the one RFC 7946 wants — is
* the negative case.
*/
Tff7b72private Tff7b72fun Td2a8ffListTff7b72<Te6edf3GeoPositionTff7b72>Tb4b4b4.Te6edf3isCounterClockwiseTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657Boolean Tb4b4b4{
Tff7b72var Te6edf3sum Tff7b72= T79c0ff0.0
Tff7b72for Tb4b4b4(Te6edf3index Tff7b72in Te6edf3indicesTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3from Tff7b72= Tff7b72thisTff7b72[Te6edf3indexTff7b72]
Tff7b72val Te6edf3to Tff7b72= Tff7b72thisTff7b72[Tb4b4b4(Te6edf3index Tff7b72+ T79c0ff1Tb4b4b4) Tff7b72% Te6edf3sizeTff7b72]
Te6edf3sum Tff7b72+Tff7b72= Tb4b4b4(Te6edf3toTb4b4b4.Te6edf3longitude Tff7b72- Te6edf3fromTb4b4b4.Te6edf3longitudeTb4b4b4) Tff7b72* Tb4b4b4(Te6edf3toTb4b4b4.Te6edf3latitude Tff7b72+ Te6edf3fromTb4b4b4.Te6edf3latitudeTb4b4b4)
Tb4b4b4}
Tff7b72return Te6edf3sum Tff7b72< T79c0ff0
Tb4b4b4}
T8b949e/**
* The line broken wherever consecutive positions jump more than half the world in longitude.
*
* Such a jump is the short way across ±180°, not a journey the long way round, so each crossing ends one
* segment on the meridian and starts the next on the other side at the same latitude.
*
* Polygons are deliberately left whole: cutting a ring means splitting it into several rings, and no import this app
* has seen needs it. A polygon spanning the antimeridian will still draw the long way round.
*/
Tff7b72private Tff7b72fun Td2a8ffListTff7b72<Te6edf3GeoPositionTff7b72>Tb4b4b4.Te6edf3splitAtAntimeridianTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3ListTff7b72<Te6edf3GeoPositionTff7b72>Tff7b72> Tb4b4b4{
Tff7b72val Te6edf3segments Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3ListTff7b72<Te6edf3GeoPositionTff7b72>Tff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3current Tff7b72= Te6edf3mutableListOfTb4b4b4(Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72for Tb4b4b4(Te6edf3next Tff7b72in Te6edf3dropTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3previous Tff7b72= Te6edf3currentTb4b4b4.Te6edf3lastTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3delta Tff7b72= Te6edf3nextTb4b4b4.Te6edf3longitude Tff7b72- Te6edf3previousTb4b4b4.Te6edf3longitude
Tff7b72if Tb4b4b4(Te6edf3absTb4b4b4(Te6edf3deltaTb4b4b4) Tff7b72> Te6edf3HALF_TURNTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3exit Tff7b72= Tff7b72if Tb4b4b4(Te6edf3delta Tff7b72> T79c0ff0Tb4b4b4) Tff7b72-Te6edf3HALF_TURN Tff7b72else Te6edf3HALF_TURN
Tff7b72val Te6edf3latitude Tff7b72= Te6edf3crossingLatitudeTb4b4b4(Te6edf3previousTb4b4b4, Te6edf3nextTb4b4b4, Te6edf3exitTb4b4b4)
Te6edf3current Tff7b72+Tff7b72= Te6edf3GeoPositionTb4b4b4(Te6edf3exitTb4b4b4, Te6edf3latitudeTb4b4b4)
Te6edf3segments Tff7b72+Tff7b72= Te6edf3current
Te6edf3current Tff7b72= Te6edf3mutableListOfTb4b4b4(Te6edf3GeoPositionTb4b4b4(Tff7b72-Te6edf3exitTb4b4b4, Te6edf3latitudeTb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3current Tff7b72+Tff7b72= Te6edf3next
Tb4b4b4}
Te6edf3segments Tff7b72+Tff7b72= Te6edf3current
Tff7b72return Te6edf3segments
Tb4b4b4}
T8b949e/** The latitude at which the segment meets the antimeridian, interpolated along the short way round. */
Tff7b72private Tff7b72fun Td2a8ffcrossingLatitudeTb4b4b4(Te6edf3fromTb4b4b4: Te6edf3GeoPositionTb4b4b4, Te6edf3toTb4b4b4: Te6edf3GeoPositionTb4b4b4, Te6edf3exitTb4b4b4: Tffa657DoubleTb4b4b4)Tb4b4b4: Tffa657Double Tb4b4b4{
Tff7b72val Te6edf3delta Tff7b72= Te6edf3toTb4b4b4.Te6edf3longitude Tff7b72- Te6edf3fromTb4b4b4.Te6edf3longitude
Tff7b72val Te6edf3shortWay Tff7b72= Te6edf3delta Tff7b72- Te6edf3FULL_TURN Tff7b72* Te6edf3signTb4b4b4(Te6edf3deltaTb4b4b4)
Tff7b72val Te6edf3fraction Tff7b72= Tb4b4b4(Tb4b4b4(Te6edf3exit Tff7b72- Te6edf3fromTb4b4b4.Te6edf3longitudeTb4b4b4) Tff7b72/ Te6edf3shortWayTb4b4b4)Tb4b4b4.Te6edf3coerceInTb4b4b4(T79c0ff0.0Tb4b4b4, T79c0ff1.0Tb4b4b4)
Tff7b72return Te6edf3fromTb4b4b4.Te6edf3latitude Tff7b72+ Te6edf3fraction Tff7b72* Tb4b4b4(Te6edf3toTb4b4b4.Te6edf3latitude Tff7b72- Te6edf3fromTb4b4b4.Te6edf3latitudeTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72const Tff7b72val Te6edf3HALF_TURN Tff7b72= T79c0ff1T79c0ff8T79c0ff0.0
Tff7b72private Tff7b72const Tff7b72val Te6edf3FULL_TURN Tff7b72= T79c0ff3T79c0ff6T79c0ff0.0
T8b949e/**
* KML coordinates are whitespace-separated `lon,lat[,alt]` tuples; GeoJSON wants `[lon, lat]` pairs.
*
* Altitude is dropped rather than carried: nothing on either map reads it, and a third ordinate would make every
* downstream bounding-box calculation handle a case it never needs to.
*/
Tff7b72internal Tff7b72fun Td2a8ffparseCoordinatesTb4b4b4(Te6edf3rawTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3GeoPositionTff7b72>Tff7b72? Tb4b4b4{
Tff7b72val Te6edf3positions Tff7b72=
Te6edf3rawTb4b4b4.Te6edf3trimTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3splitTb4b4b4(Te6edf3WHITESPACETb4b4b4)Tb4b4b4.Te6edf3mapNotNull Tb4b4b4{ Te6edf3tuple Tff7b72-Tff7b72>
Tff7b72val Te6edf3parts Tff7b72= Te6edf3tupleTb4b4b4.Te6edf3splitTb4b4b4(Ta5d6ff','Tb4b4b4)
Tff7b72val Te6edf3longitude Tff7b72= Te6edf3partsTb4b4b4.Te6edf3getOrNullTb4b4b4(T79c0ff0Tb4b4b4)Tff7b72?.Te6edf3trimTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3toDoubleOrNullTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3takeIf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3isValidOrdinateTb4b4b4(Te6edf3MAX_LONGITUDETb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3latitude Tff7b72= Te6edf3partsTb4b4b4.Te6edf3getOrNullTb4b4b4(T79c0ff1Tb4b4b4)Tff7b72?.Te6edf3trimTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3toDoubleOrNullTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3takeIf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3isValidOrdinateTb4b4b4(Te6edf3MAX_LATITUDETb4b4b4) Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3longitude Tff7b72=Tff7b72= Tff7b72null Tff7b72|Tff7b72| Te6edf3latitude Tff7b72=Tff7b72= Tff7b72nullTb4b4b4) Tff7b72null Tff7b72else Te6edf3GeoPositionTb4b4b4(Te6edf3longitudeTb4b4b4, Te6edf3latitudeTb4b4b4)
Tb4b4b4}
Tff7b72return Te6edf3positionsTb4b4b4.Te6edf3ifEmpty Tb4b4b4{ Tff7b72null Tb4b4b4}
Tb4b4b4}
T8b949e/** GeoJSON's ordinate bounds, per RFC 7946. */
Tff7b72private Tff7b72const Tff7b72val Te6edf3MAX_LONGITUDE Tff7b72= T79c0ff1T79c0ff8T79c0ff0.0
Tff7b72private Tff7b72const Tff7b72val Te6edf3MAX_LATITUDE Tff7b72= T79c0ff9T79c0ff0.0
T8b949e/**
* Whether an ordinate is one this converter will write, checked before the number reaches the JSON.
*
* `toDoubleOrNull` accepts "NaN" and "Infinity", and these coordinates are interpolated into the output as text — so a
* single such ordinate emits a bare `NaN` token and makes the *whole* converted file unparseable, taking every other
* placemark in it down too. That is the same shape of failure as writing `0,498` for a decimal under a comma-decimal
* locale. Out-of-range values go on the same pass: a coordinate outside RFC 7946's bounds is corrupt input rather than
* a place on Earth.
*/
Tff7b72private Tff7b72fun Te6edf3DoubleTb4b4b4.Td2a8ffisValidOrdinateTb4b4b4(Te6edf3limitTb4b4b4: Tffa657DoubleTb4b4b4)Tb4b4b4: Tffa657Boolean Tff7b72= Te6edf3isFiniteTb4b4b4(Tb4b4b4) Tff7b72&Tff7b72& Tff7b72this Tff7b72in Tff7b72-Te6edf3limitTb4b4b4.Tb4b4b4.Te6edf3limit
T8b949e/** Whitespace between KML coordinate tuples. */
Tff7b72private Tff7b72val Te6edf3WHITESPACE Tff7b72= Te6edf3RegexTb4b4b4(Ta5d6ff"Ta5d6ff\\Ta5d6ffs+Ta5d6ff"Tb4b4b4)
T8b949e/** A GeoJSON ring needs three distinct positions before it can be closed into a polygon. */
Tff7b72internal Tff7b72const Tff7b72val Te6edf3MIN_RING_POSITIONS Tff7b72= T79c0ff3
Tff7b72internal Tff7b72const Tff7b72val Te6edf3MIN_LINE_POSITIONS Tff7b72= T79c0ff2
Served by rngit 1.5.3 - Generated in 0.05s